package com.ruoyi.my.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.my.domain.MyBucket;
import com.ruoyi.my.domain.MyFile;
import com.ruoyi.my.dto.FileModifyDTO;
import com.ruoyi.my.mapper.MyBucketMapper;
import com.ruoyi.my.mapper.MyFileMapper;
import com.ruoyi.my.service.MyFileService;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class MyFileServiceImpl implements MyFileService {

    @Resource
    private MinioClient minioClient;

    @Autowired
    MyFileMapper myFileMapper;

    @Autowired
    MyBucketMapper myBucketMapper;

    /**
     * 根据文件信息查询文件列表
     * @return 文件列表
     */
    @Override
    public List<MyFile> selectFileList(MyFile myFile) {
        return myFileMapper.selectList(new LambdaQueryWrapper<MyFile>()
                .like(myFile.getName() != null && myFile.getName() != "", MyFile::getName, myFile.getName())
                .eq(myFile.getBucketName() != null && myFile.getBucketName() != "", MyFile::getBucketName, myFile.getBucketName())
                .eq(myFile.getFirstDirectory() != null && myFile.getFirstDirectory() != "", MyFile::getFirstDirectory, myFile.getFirstDirectory())
                .eq(myFile.getSecondDirectory() != null && myFile.getSecondDirectory() != "", MyFile::getSecondDirectory, myFile.getSecondDirectory())
                .eq(myFile.getSectionProjectId() != null, MyFile::getSectionProjectId, myFile.getSectionProjectId())
                .eq(myFile.getFileType() != null && myFile.getFileType() != "", MyFile::getFileType, myFile.getFileType()));
    }

    /**
     * 批量上传文件
     * @return 插入结果信息数组
     */
    @Override
    public List<String> insert(List<MultipartFile> files, MyFile myFile) {
        try {
            List<String> list = new ArrayList<>();
            //minio中bucket的实际名称，用于向bucket存放文件。myFile.getBucketName()获得的为数据库文件表中存放位置名称
            String bucketName = myBucketMapper.selectOne(new LambdaQueryWrapper<MyBucket>()
                    .eq(MyBucket::getModuleName, myFile.getBucketName())).getName();
            if (!isBucketExits(bucketName)) return null;
            //添加文件
            for (MultipartFile file : files){
                String filename = file.getOriginalFilename();
                //二级目录下的文件
                String fileFullName = myFile.getFirstDirectory() + '/' + filename;
                //先检查该文件在目录中是否存在
                MyFile flag = myFileMapper.selectOne(new LambdaQueryWrapper<MyFile>()
                        .eq(MyFile::getBucketName, myFile.getBucketName())
                        .eq(MyFile::getName, filename)
                        .eq(MyFile::getFirstDirectory, myFile.getFirstDirectory()));
                if (flag == null){
                    //文件名后缀
                    String suffix = filename.substring(filename.lastIndexOf(".")+1);
                    minioClient.putObject(PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(fileFullName)
                            .stream(file.getInputStream(),file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build()
                    );
                    myFile.setName(filename);
                    myFile.setFileType(suffix.toLowerCase());
                    myFileMapper.insert(myFile);
                }else {
                    //同一bucket下文件名不能重复，返回重复的文件名列表
                    list.add(filename);
                }
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 批量删除文件
     * @return 删除结果信息
     */
    @Override
    public String remove(List<Integer> ids) {
        try {
            List<MyFile> myFiles = myFileMapper.selectBatchIds(ids);
            //Key:bucket名称 Value:该bucket下待删除文件名
            Map<String, List<DeleteObject>> map = new HashMap<>();
            for (MyFile myFile : myFiles) {
                if (map.containsKey(myFile.getBucketName())){
                    map.get(myFile.getBucketName()).add(new DeleteObject(myFile.getFirstDirectory() + '/' + myFile.getName()));
                } else{
                    List<DeleteObject> tmp = new LinkedList<>();
                    tmp.add(new DeleteObject(myFile.getFirstDirectory() + '/' + myFile.getName()));
                    map.put(myFile.getBucketName(), tmp);
                }
            }
            //删除bucket中的文件
            for (Map.Entry<String, List<DeleteObject>> entry : map.entrySet()){
                String bucketName = myBucketMapper.selectOne(new LambdaQueryWrapper<MyBucket>()
                        .eq(MyBucket::getModuleName, entry.getKey())).getName();
                Iterable<Result<DeleteError>> results = minioClient.removeObjects(
                                RemoveObjectsArgs.builder().bucket(bucketName).objects(entry.getValue()).build());
                for (Result<DeleteError> result : results) {}  //请不要删除此行代码，该行执行迭代删除任务
            }
            //删除数据库中的文件
            myFileMapper.deleteBatchIds(ids);
            return "ok";
        }catch (Exception e){
            e.printStackTrace();
            return e.getMessage();
        }
    }

    /**
     * 下载文件
     * @return 字节输入流以及文件信息
     */
    @Override
    public String download(Integer id) {
        MyFile myFile = myFileMapper.selectById(id);
        if (myFile == null) return null;
        String fileName = myFile.getName();
        String bucketName = myBucketMapper.selectOne(new LambdaQueryWrapper<MyBucket>()
                .eq(MyBucket::getModuleName, myFile.getBucketName())).getName();
        if (!isBucketExits(bucketName)) return null;
        //二级目录下的文件
        String fileFullName = myFile.getFirstDirectory() + '/' + fileName;
        try {
            Map<String, String> reqParams = new HashMap<>();
            //文件下载
            reqParams.put("response-content-type", "application/octet-stream");
            return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(bucketName)
                    .object(fileFullName)
                    .expiry(2, TimeUnit.HOURS) //有效时间2小时
                    .extraQueryParams(reqParams)
                    .build());
        }catch (Exception e){
            e.getMessage();
        }
        return null;
    }

    /**
     * 预览文件
     * @return 预览文件的url，可直接访问
     */
    @Override
    public String preview(Integer id) {
        MyFile myFile = myFileMapper.selectById(id);
        if (myFile == null) return null;
        String fileName = myFile.getName();
        String bucketName = myBucketMapper.selectOne(new LambdaQueryWrapper<MyBucket>()
                .eq(MyBucket::getModuleName, myFile.getBucketName())).getName();
        if (!isBucketExits(bucketName)) return null;
        //二级目录下的文件
        String fileFullName = myFile.getFirstDirectory() + '/' + fileName;
        try {
            String fileContentType = getFileContentType(fileName);
            Map<String, String> reqParams = new HashMap<>();
            //当前可以预览jpeg png jpg mp4 html css js pdf等类型文件 如果不是这些文件，将直接下载文件
            reqParams.put("response-content-type", fileContentType);
            return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucketName)
                            .object(fileFullName)
                            .expiry(2, TimeUnit.HOURS) //有效时间2小时
                            .extraQueryParams(reqParams)
                            .build());
        }catch (Exception e){
            e.getMessage();
        }
        return null;
    }


    /**
     * 2022年5月23日
     * 修改文件
     * @param file 待修改的文件
     * @param fileModifyDTO 修改文件的信息
     * @return
     */
    @Transactional(rollbackFor = Throwable.class, isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    @Override
    public String modify(MultipartFile file, FileModifyDTO fileModifyDTO) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

        // 1. 获取新文件的文件名，文件类型，原文件的文件名，以及bucketName的英文版
        String newFileName = file.getOriginalFilename();
        String newFullFileName = fileModifyDTO.getFirstDirectory() + "/" + newFileName;
        String newFileType = newFileName.substring(newFileName.lastIndexOf(".")+1).toLowerCase();
        String bucketEnglishName = myBucketMapper.selectOne(new LambdaQueryWrapper<MyBucket>()
                .eq(MyBucket::getModuleName, fileModifyDTO.getBucketName())).getName();
        String oldFileName = myFileMapper.selectById(fileModifyDTO.getId()).getName();
        String oldFullFileName = fileModifyDTO.getFirstDirectory() + "/" + newFileName + oldFileName;

        // 2. 查询my_file表，根据bucketName,FirstDirectory和fileName，判断修改的文件名是否已经存在
        MyFile sameNameFile = myFileMapper.selectOne(new LambdaQueryWrapper<MyFile>()
                .eq(MyFile::getBucketName, fileModifyDTO.getBucketName())
                .eq(MyFile::getName, newFileName)
                .eq(MyFile::getFirstDirectory, fileModifyDTO.getFirstDirectory()));

        boolean isSameFileExist = false;    // 判断同名文件是否存在
        if (sameNameFile != null){
            isSameFileExist = true;
        }


        // 3 文件名不存在
        if (!isSameFileExist){

            // 3.1 删除my_file表中的原文件，根据FileModifyDTO中的id
            myFileMapper.deleteById(fileModifyDTO.getId());

            // 3.2 调用minio的接口，根据bucketName和fileName删除原文件
            minioClient.removeObject(
                    RemoveObjectArgs.builder().bucket(bucketEnglishName).object(oldFullFileName).build());

            // 3.3 调用minio的接口，向bucketName插入新文件
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketEnglishName)
                    .object(newFullFileName)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build()
            );

            // 3.4 向my_file表中插入新记录
            MyFile myFileForInsert = new MyFile();
            myFileForInsert.setName(newFileName);
            myFileForInsert.setFileType(newFileType);
            myFileForInsert.setBucketName(fileModifyDTO.getBucketName());
            myFileForInsert.setFirstDirectory(fileModifyDTO.getFirstDirectory());
            myFileForInsert.setSecondDirectory(fileModifyDTO.getSecondDirectory());
            myFileForInsert.setSectionProjectId(fileModifyDTO.getSectionProjectId());

            myFileMapper.insert(myFileForInsert);

            return "修改成功";

        }

        // 4 文件名已经存在
        else{
            // 4.1 允许覆盖
            if (fileModifyDTO.isOverwrite()){
                // 4.1.1 删除my_file表中的原文件，根据FileModifyDTO中的id
                myFileMapper.deleteById(fileModifyDTO.getId());

                // 4.1.2 调用minio的接口，根据bucketName和fileName删除原文件
                minioClient.removeObject(
                        RemoveObjectArgs.builder().bucket(bucketEnglishName).object(oldFullFileName).build());

                // 4.1.3 删除my_file表中的同名文件
                myFileMapper.delete(new LambdaQueryWrapper<MyFile>()
                        .eq(MyFile::getName,newFileName)
                        .eq(MyFile::getFirstDirectory, fileModifyDTO.getFirstDirectory())
                        .eq(MyFile::getBucketName, fileModifyDTO.getBucketName()));

                // 4.1.4 调用minio的接口，根据bucketName和fileName删除同名文件
                minioClient.removeObject(
                        RemoveObjectArgs.builder().bucket(bucketEnglishName).object(newFullFileName).build());

                // 4.1.5 调用minio的接口，向bucketName插入新文件
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(bucketEnglishName)
                        .object(newFullFileName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build()
                );

                // 4.1.6 向my_file表中插入记录
                MyFile myFileForInsert = new MyFile();
                myFileForInsert.setName(newFileName);
                myFileForInsert.setFileType(newFileType);
                myFileForInsert.setBucketName(fileModifyDTO.getBucketName());
                myFileForInsert.setFirstDirectory(fileModifyDTO.getFirstDirectory());
                myFileForInsert.setSecondDirectory(fileModifyDTO.getSecondDirectory());
                myFileForInsert.setSectionProjectId(fileModifyDTO.getSectionProjectId());

                myFileMapper.insert(myFileForInsert);

                return "覆盖成功";
            }
            // 4.2 不要覆盖
            else {
                // do nothing
                return "存在同名文件";
            }

        }

    }


    /**
     * 2022年5月26日
     * 获取文件类型列表
     * @return 文件类型列表
     */
    @Override
    public List<tempFileType> fetchFileTypes(String menu, String name) {
        List<MyFile> distinct_file_type = myFileMapper.selectList(new QueryWrapper<MyFile>()
                .eq("bucket_name",menu)
                .eq("first_directory", name)
                .select("DISTINCT file_type"));
        List<tempFileType> fileTypeList = new ArrayList<>();
        for (MyFile myFile : distinct_file_type) {
            fileTypeList.add(new tempFileType(myFile.getFileType(), myFile.getFileType()));
        }
        return fileTypeList;
    }


    /**
     * 查看bucket是否存在
     * @return 文件是否存在
     */
    private boolean isBucketExits(String bucketName){
        try {
            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        } catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 获得文件类型
     * @return 返回文件类型
     */
    private static String getFileContentType(String fileName) {
        String returnFileName = fileName.substring(fileName.lastIndexOf("."));
        if (returnFileName != null) {
            if (returnFileName.equals(".jpeg") || returnFileName.equals(".png") || returnFileName.equals(".jpg") ||
                returnFileName.equals(".JPEG") || returnFileName.equals(".PNG") || returnFileName.equals(".JPG")) {
                return "image/jpeg";
            } else if (returnFileName.equals(".mp4")) {
                return "video/mp4";
            } else if (returnFileName.equals(".html")) {
                return "text/html";
            } else if (returnFileName.equals(".css")) {
                return "text/css";
            } else if (returnFileName.equals(".js")) {
                return "application/javascript";
            } else if (returnFileName.equals(".pdf")) {
                return "application/pdf";
            } else {
                return "application/octet-stream";
            }
        }
        return null;
    }


    /**
     * 临时类，用于返回给前端显示文件类型
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class tempFileType{
        private String id;
        private String name;
    }


}
